//go:build integration

package integration

import (
	"bytes"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/eggsampler/acme/v3"

	"github.com/letsencrypt/boulder/test"
)

// randomDomain creates a random domain name for testing.
func randomDomain(t *testing.T) string {
	t.Helper()

	var bytes [4]byte
	_, err := rand.Read(bytes[:])
	if err != nil {
		test.AssertNotError(t, err, "Failed to generate random domain")
	}
	return fmt.Sprintf("%x.mail.com", bytes[:])
}

// getOAuthToken queries the pardot-test-srv for the current OAuth token.
func getOAuthToken(t *testing.T) string {
	t.Helper()

	data, err := os.ReadFile("test/secrets/salesforce_client_id")
	test.AssertNotError(t, err, "Failed to read Salesforce client ID")
	clientId := string(data)

	data, err = os.ReadFile("test/secrets/salesforce_client_secret")
	test.AssertNotError(t, err, "Failed to read Salesforce client secret")
	clientSecret := string(data)

	httpClient := http.DefaultClient
	resp, err := httpClient.PostForm("http://localhost:9601/services/oauth2/token", url.Values{
		"grant_type":    {"client_credentials"},
		"client_id":     {strings.TrimSpace(clientId)},
		"client_secret": {strings.TrimSpace(clientSecret)},
	})
	test.AssertNotError(t, err, "Failed to fetch OAuth token")
	test.AssertEquals(t, resp.StatusCode, http.StatusOK)
	defer resp.Body.Close()

	var response struct {
		AccessToken string `json:"access_token"`
	}
	decoder := json.NewDecoder(resp.Body)
	err = decoder.Decode(&response)
	test.AssertNotError(t, err, "Failed to decode OAuth token")
	return response.AccessToken
}

// getCreatedContacts queries the pardot-test-srv for the list of created
// contacts.
func getCreatedContacts(t *testing.T, token string) []string {
	t.Helper()

	httpClient := http.DefaultClient
	req, err := http.NewRequest("GET", "http://localhost:9602/contacts", nil)
	test.AssertNotError(t, err, "Failed to create request")
	req.Header.Set("Authorization", "Bearer "+token)

	resp, err := httpClient.Do(req)
	test.AssertNotError(t, err, "Failed to query contacts")
	test.AssertEquals(t, resp.StatusCode, http.StatusOK)
	defer resp.Body.Close()

	var got struct {
		Contacts []string `json:"contacts"`
	}
	decoder := json.NewDecoder(resp.Body)
	err = decoder.Decode(&got)
	test.AssertNotError(t, err, "Failed to decode contacts")
	return got.Contacts
}

// assertAllContactsReceived waits for the expected contacts to be received by
// pardot-test-srv. Retries every 50ms for up to 2 seconds and fails if the
// expected contacts are not received.
func assertAllContactsReceived(t *testing.T, token string, expect []string) {
	t.Helper()

	for attempt := range 20 {
		if attempt > 0 {
			time.Sleep(50 * time.Millisecond)
		}
		got := getCreatedContacts(t, token)

		allFound := true
		for _, e := range expect {
			if !slices.Contains(got, e) {
				allFound = false
				break
			}
		}
		if allFound {
			break
		}
		if attempt >= 19 {
			t.Fatalf("Expected contacts=%v to be received by pardot-test-srv, got contacts=%v", expect, got)
		}
	}
}

// TestContactsSentForNewAccount tests that contacts are dispatched to
// pardot-test-srv by the email-exporter when a new account is created.
func TestContactsSentForNewAccount(t *testing.T) {
	t.Parallel()

	if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" {
		t.Skip("Test requires WFE to be configured to use email-exporter")
	}

	token := getOAuthToken(t)
	domain := randomDomain(t)

	tests := []struct {
		name           string
		contacts       []string
		expectContacts []string
	}{
		{
			name:           "Single email",
			contacts:       []string{"mailto:example@" + domain},
			expectContacts: []string{"example@" + domain},
		},
		{
			name:           "Multiple emails",
			contacts:       []string{"mailto:example1@" + domain, "mailto:example2@" + domain},
			expectContacts: []string{"example1@" + domain, "example2@" + domain},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			c, err := acme.NewClient("http://boulder.service.consul:4001/directory")
			if err != nil {
				t.Fatalf("failed to connect to acme directory: %s", err)
			}

			acctKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
			if err != nil {
				t.Fatalf("failed to generate account key: %s", err)
			}

			_, err = c.NewAccount(acctKey, false, true, tt.contacts...)
			test.AssertNotError(t, err, "Failed to create initial account with contacts")
			assertAllContactsReceived(t, token, tt.expectContacts)
		})
	}
}

// getCreatedCases queries the pardot-test-srv for the list of created cases.
// Fails the test on error.
func getCreatedCases(t *testing.T, token string) []map[string]any {
	t.Helper()

	req, err := http.NewRequest("GET", "http://localhost:9601/cases", nil)
	test.AssertNotError(t, err, "Failed to create cases request")
	req.Header.Set("Authorization", "Bearer "+token)

	resp, err := http.DefaultClient.Do(req)
	test.AssertNotError(t, err, "Failed to query cases")
	test.AssertEquals(t, resp.StatusCode, http.StatusOK)
	defer resp.Body.Close()

	var got struct {
		Cases []map[string]any `json:"cases"`
	}
	err = json.NewDecoder(resp.Body).Decode(&got)
	test.AssertNotError(t, err, "Failed to decode cases")
	return got.Cases
}

// createCase sends a request to create a new case via pardot-test-srv and
// returns the HTTP status code and response body. Fails the test on error.
func createCase(t *testing.T, token string, payload map[string]any) (int, []byte) {
	t.Helper()

	b, err := json.Marshal(payload)
	test.AssertNotError(t, err, "Failed to marshal case payload")

	req, err := http.NewRequest(
		"POST",
		"http://localhost:9601/services/data/v64.0/sobjects/Case",
		bytes.NewReader(b),
	)
	test.AssertNotError(t, err, "Failed to create case POST request")
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	test.AssertNotError(t, err, "Failed to POST case")
	defer resp.Body.Close()

	var body bytes.Buffer
	_, err = body.ReadFrom(resp.Body)
	test.AssertNotError(t, err, "Failed to read case response body")

	return resp.StatusCode, body.Bytes()
}

func TestCasesAPISuccess(t *testing.T) {
	t.Parallel()

	token := getOAuthToken(t)

	status, _ := createCase(t, token, map[string]any{
		"Subject":     "Integration Test Case",
		"Description": "Created by integration test",
		"Origin":      "Web",
	})
	test.AssertEquals(t, status, http.StatusCreated)

	// Verify it was recorded by the fake server.
	cases := getCreatedCases(t, token)
	found := false
	for _, c := range cases {
		if c["Subject"] == "Integration Test Case" && c["Origin"] == "Web" {
			found = true
			break
		}
	}
	if !found {
		t.Fatalf("Expected created case to be present; got cases=%s", cases)
	}
}

func TestCasesAPIMissingOrigin(t *testing.T) {
	t.Parallel()

	token := getOAuthToken(t)

	// Missing Origin should be rejected by the fake server.
	status, body := createCase(t, token, map[string]any{
		"Subject":     "Missing Origin Case",
		"Description": "Should fail",
	})
	test.AssertEquals(t, status, http.StatusBadRequest)
	test.AssertContains(t, string(body), "Missing required field: Origin")
}

func TestCasesAPIUsingSFE(t *testing.T) {
	t.Parallel()

	if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" {
		t.Skip("Test requires SFE to be configured to use email-exporter")
	}

	token := getOAuthToken(t)

	body, err := json.Marshal(map[string]any{
		"rateLimit": "NewOrdersPerAccount",
		"fields": map[string]string{
			"subscriberAgreement": "true",
			"privacyPolicy":       "true",
			"mailingList":         "false",
			"fundraising":         "Yes, email me more information.",
			"emailAddress":        "test@foo.bar",
			"organization":        "Big Host Inc.",
			"useCase":             strings.Repeat("x", 60),
			"tier":                "1000",
			"accountURI":          "https://acme-v02.api.letsencrypt.org/acme/acct/12345",
		},
	})
	test.AssertNotError(t, err, "marshal override payload")

	req, err := http.NewRequest(http.MethodPost, "http://localhost:4003/sfe/v1/overrides/submit-override-request", bytes.NewReader(body))
	test.AssertNotError(t, err, "creating override request")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	test.AssertNotError(t, err, "POSTing override request to SFE")
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusCreated {
		var buf bytes.Buffer
		_, err = buf.ReadFrom(resp.Body)
		test.AssertNotError(t, err, "reading SFE response body")
		t.Errorf("unexpected SFE status=%d with body=%s", resp.StatusCode, buf.String())
	}

	timeout := 3 * time.Second
	interval := 10 * time.Millisecond

	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	timer := time.NewTimer(timeout)
	defer timer.Stop()

	for {
		select {
		case <-ticker.C:
			cases := getCreatedCases(t, token)
			for _, c := range cases {
				if c["Subject"] == "NewOrdersPerAccount rate limit override request for Big Host Inc." &&
					c["Origin"] == "Web" &&
					c["ContactEmail"] == "test@foo.bar" &&
					c["Organization__c"] == "Big Host Inc." &&
					c["Rate_Limit_Name__c"] == "NewOrdersPerAccount" &&
					c["Rate_Limit_Tier__c"] == "1000" {
					return
				}
			}
		case <-timer.C:
			t.Fatalf("expected Case never created within %s", timeout)
		}
	}
}
